﻿#region Using References

using System;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using Beaker.OpenCube.IO;
using Beaker.OpenCube.Nbt;

#endregion

namespace Beaker.OpenCube.Anvil
{

	/// <summary>
	/// A Reader for reading region files
	/// </summary>
	public sealed class RegionReader
	{
		private readonly Stream _stream;
		private readonly BigEndianBinaryReader _reader;

		public RegionReader( Stream stream )
		{
			Contract.Requires<ArgumentNullException>( stream != null );
			Contract.Requires<NotSupportedException>( stream.CanSeek, "Stream must be seekable to be used with the RegionReader" );
			Contract.Ensures( _stream == stream );
			Contract.Ensures( _reader != null );

			_stream = stream;
			_reader = new BigEndianBinaryReader( stream );
		}

		[ContractInvariantMethod]
		private void ContractInvariant()
		{
			Contract.Invariant( _stream != null );
			Contract.Invariant( _reader != null );
		}

		private bool ReadLocation( int index, out long offset, out int length )
		{
			Contract.Requires<ArgumentOutOfRangeException>( index >= 0 );
			Contract.Requires<ArgumentOutOfRangeException>( index < 4096 );
			Contract.Ensures( !Contract.Result<bool>() || Contract.ValueAtReturn( out offset ) >= 8192 );
			Contract.Ensures( !Contract.Result<bool>() || Contract.ValueAtReturn( out length ) >= 4096 );

			// Location information for a chunk consists of four bytes split into two fields: 
			// the first three bytes are a (big-endian) offset in 4KiB sectors from the start of the file, 
			// and a remaining byte which gives the length of the chunk (also in 4KiB sectors, rounded up). 
			// If a chunk isn't present in the region file (e.g. because it hasn't been generated or migrated yet), both fields will be zero.
			_stream.Seek( index, SeekOrigin.Begin );

			offset = _reader.ReadInt24() * 4096;
			length = _reader.ReadByte() * 4096;

			if( offset == 0 && length == 0 )
				return false;

			return true;
		}

		private int ReadTimestamp( int index )
		{
			Contract.Requires<ArgumentOutOfRangeException>( index >= 0 );
			Contract.Requires<ArgumentOutOfRangeException>( index < 4092 );

			_stream.Seek( 4096 + index, SeekOrigin.Begin );
			return _reader.ReadInt32();
		}

		private NbtDictionary ReadChunckData( long offset, int maxLength )
		{
			Contract.Requires<ArgumentOutOfRangeException>( offset >= 8192 );

			_stream.Seek( offset, SeekOrigin.Begin );

			int length = _reader.ReadInt32();
			var compressionType = (CompressionType)_reader.ReadByte();

			Stream tempStream = _stream;

			// GZip is supported by the default NbtReader used
			// but if zlib is used, we need to wrap the stream in the correct decompressor
			if( compressionType == CompressionType.Zlib )
			{
				// If you prefer to read Zlib-compressed chunk data with Deflate (RFC1951), 
				// just skip the first two bytes
				_stream.Seek( 2, SeekOrigin.Current );
				tempStream = new DeflateStream( _stream, CompressionMode.Decompress );
			}

			return NbtReader.Deserialize( tempStream );
		}

		/// <summary>
		/// Gets the name of the chunk file for the given coordinates
		/// </summary>
		/// <param name="x">The x chunk coordinate.</param>
		/// <param name="z">The y chunk coordinate.</param>
		/// <returns></returns>
		public static string GetChunkFileName( int x, int z )
		{
			return "r."
			       + ( x / 32 ).ToString( CultureInfo.InvariantCulture )
			       + "." + ( z / 32 ).ToString( CultureInfo.InvariantCulture ) + ".mca";
		}

		/// <summary>
		/// Convert chunck coordinates to a local index.
		/// </summary>
		/// <param name="x">The x chunk coordinate.</param>
		/// <param name="z">The z chunk coordinate.</param>
		/// <returns>The local index of the coordinates.</returns>
		private static int ToIndex( int x, int z )
		{
			Contract.Ensures( Contract.Result<int>() >= 0 );
			Contract.Ensures( Contract.Result<int>() < 8192 );

			// The location in the region file of a chunk at (x, z) (in chunk coordinates)
			// can be found at byte offset 4 * ((x mod 32) + (z mod 32) * 32) in its region file. 
			// In case the values of x or z are negative, subtract their absolute value from 31 after calculating the modulo. 
			x %= 32;
			if( x < 0 )
				x = +31;

			z %= 32;
			if( z < 0 )
				z += 31;

			return 4 * ( x + z * 32 );  // *4 because both location and timestamp are 4 bytes
		}

		public static NbtDictionary ReadChunk( Stream stream, int x, int z )
		{
			Contract.Requires<ArgumentOutOfRangeException>( stream != null );
			Contract.Requires<NotSupportedException>( stream.CanSeek, "Stream must be seekable to be used with the RegionReader" );

			var reader = new RegionReader( stream );

			int index = ToIndex( x, z );

			// read location
			long offset;
			int length;
			if( !reader.ReadLocation( index, out offset, out length ) )
				return null;

			// read timestamp
			int timestamp = reader.ReadTimestamp( index );

			// read chunk data
			NbtDictionary dictionary = reader.ReadChunckData( offset, length );

			// add timestamp to chunk data
			dictionary["timestamp"] = timestamp;

			return dictionary;
		}
	}
}